package fetch

import (
	"context"
	"fmt"
	"github.com/shirou/gopsutil/cpu"
	"github.com/shirou/gopsutil/disk"
	"github.com/shirou/gopsutil/mem"
	"github.com/shirou/gopsutil/net"
	"go.uber.org/zap"
	. "monitor-agent/configs"
	. "monitor-agent/internal/common"
	. "monitor-agent/internal/report"
	"monitor-agent/internal/utils"
	. "monitor-agent/pkg/logging"
	"monitor-agent/registry"
	"runtime"
	"strconv"
	"time"
)

type IPInfo struct {
	Code int `json:"code"`
	Data IP  `json:"data"`
}

type IP struct {
	Country   string `json:"country"`
	CountryId string `json:"country_id"`
	Area      string `json:"area"`
	AreaId    string `json:"area_id"`
	Region    string `json:"region"`
	RegionId  string `json:"region_id"`
	City      string `json:"city"`
	CityId    string `json:"city_id"`
	Isp       string `json:"isp"`
}

var localAddr string
var publicAddr string

func Fetch(r ReportServerClient, isLocal bool) {
	isProd := Config.Env.IsProd

	if !isProd {
		fmt.Println("===================")
		fmt.Printf("starting fetch, time: %s\n", time.Now().String())
	}

	dm := make(map[string]string, 7)
	if !isLocal {
		result, err := utils.GetPublicIP()
		if err != nil {
			Logger().Fatal("failed to fetch public ip info", zap.Error(err))
		}
		dm[KeyCountry] = result.Country
		dm[KeyRegion] = result.RegionName
		dm[KeyTimezone] = result.Timezone
		dm[KeyCity] = result.City
		dm[KeyISP] = result.Isp
		publicAddr = result.Query
	} else {
		publicAddr = "N/A"
	}

	dm[KeyPubAddress] = publicAddr

	var err error
	localAddr, err = utils.GetLocalIP()
	if err != nil {
		Logger().Fatal("failed to fetch local ip address")
	}

	dm[KeyLocAddress] = localAddr
	dm[KeySys] = runtime.GOOS

	cto := registry.GetSetting()

	ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(cto.FetchTimeout))
	defer cancel()

	reports := ReportReq{
		Timestamp:  time.Now().Unix(),
		Dimensions: dm,
		Requests:   nil,
		Token:      Config.Auth.Token,
	}

	temp := make(map[string]*ReportReqItem, 6)
	switchFunc := func(tm, m1, m2 string,
		fn func(ctx context.Context, isProd bool) (item1, item2 *ReportReqItem)) {
		if temp[tm] == nil {
			temp[m1], temp[m2] = fn(ctx, isProd)
		}
		reports.Requests = append(reports.Requests, temp[tm])
	}

	for _, setting := range cto.Settings {
		switch setting {
		case MetricCPURate:
			reports.Requests = fetchCPURate(ctx, reports.Requests, isProd)
		case MetricMemRate:
			reports.Requests = fetchMemRate(ctx, reports.Requests, isProd)
		case MetricDiskRead:
			switchFunc(MetricDiskRead, MetricDiskRead, MetricDiskWrite, fetchDiskIO)
		case MetricDiskWrite:
			switchFunc(MetricDiskWrite, MetricDiskRead, MetricDiskWrite, fetchDiskIO)
		case MetricDiskUsedRate:
			switchFunc(MetricDiskUsedRate, MetricDiskUsedRate, MetricDiskFreeRate, fetchDiskUsage)
		case MetricDiskFreeRate:
			switchFunc(MetricDiskFreeRate, MetricDiskUsedRate, MetricDiskFreeRate, fetchDiskUsage)
		case MetricNetSent:
			switchFunc(MetricNetSent, MetricNetSent, MetricNetRecv, fetchNetIO)
		case MetricNetRecv:
			switchFunc(MetricNetRecv, MetricNetSent, MetricNetRecv, fetchNetIO)
		default:
			Logger().Error("invalid metric type", zap.String("metric", setting))
		}
	}

	if !isProd {
		fmt.Println("sending report, ", reports.Requests)
		fmt.Println("===================")
	}

	// send the report to the backend, if resp code was not 0 than write the error msg to log file
	go handleResponse(r, sendReport(ctx, r, &reports), &reports, 0, int(cto.ResendTimes))
}

func newReportReqItem(val float64, metric string, err error) *ReportReqItem {
	var errInfo string
	if err != nil {
		errInfo = fmt.Sprintf("failed to fetch '%s', error: %s", metric, err.Error())
	}

	return &ReportReqItem{
		Metric: metric,
		Value:  fmtValue(val),
		Error:  errInfo,
	}
}

func fmtValue(value float64) float64 {
	value, _ = strconv.ParseFloat(fmt.Sprintf("%.4f", value), 64)
	return value
}

func fetchCPURate(ctx context.Context, item []*ReportReqItem, isProd bool) []*ReportReqItem {
	var tempPercent float64
	cPercents, err := cpu.PercentWithContext(ctx, time.Second*1, false)
	if err != nil {
		Logger().Error("failed to fetch cpu info", zap.Error(err))
	} else {
		tempPercent = cPercents[0]
	}
	if !isProd {
		fmt.Printf("fetched %s , get value: %f\n", MetricCPURate, tempPercent)
	}
	return append(item, newReportReqItem(tempPercent, MetricCPURate, err))
}

func fetchMemRate(ctx context.Context, item []*ReportReqItem, isProd bool) []*ReportReqItem {
	var tempPercent float64
	memInfo, err := mem.VirtualMemoryWithContext(ctx)
	if err != nil {
		Logger().Error("failed to fetch mem info", zap.Error(err))
	} else {
		tempPercent = memInfo.UsedPercent
	}
	if !isProd {
		fmt.Printf("fetched %s , get value: %f\n", MetricMemRate, tempPercent)
	}
	return append(item, newReportReqItem(tempPercent, MetricMemRate, err))
}

func fetchNetIO(ctx context.Context, isProd bool) (item1, item2 *ReportReqItem) {
	send := 0.0
	recv := 0.0
	netInfo, err := net.IOCountersWithContext(ctx, false)
	if err != nil {
		Logger().Error("failed to fetch net io info", zap.Error(err))
	} else {
		for _, netIo := range netInfo {
			send += float64(netIo.BytesSent)
			recv += float64(netIo.BytesRecv)
		}
	}
	if !isProd {
		fmt.Printf("fetched %s , get value: %f\n", MetricNetSent, send/DataUnit)
		fmt.Printf("fetched %s , get value: %f\n", MetricNetRecv, recv/DataUnit)
	}

	item1 = newReportReqItem(send/DataUnit, MetricNetSent, err)
	item2 = newReportReqItem(recv/DataUnit, MetricNetRecv, err)
	return
}

func fetchDiskUsage(ctx context.Context, isProd bool) (item1, item2 *ReportReqItem) {
	disksum := 0.0
	usedsum := 0.0
	diskInfos, err := disk.PartitionsWithContext(ctx, false)
	// todo: temporary ignore the error of 'The network path was not found.'
	if err != nil && err.Error() != "The network path was not found." {
		Logger().Error("failed to fetch disk usage info", zap.Error(err))
	} else {
		err = nil
		for _, diskInfo := range diskInfos {
			dk, err2 := disk.UsageWithContext(ctx, diskInfo.Mountpoint)
			if err2 == nil {
				disksum += float64(dk.Used) + float64(dk.Free)
				usedsum += float64(dk.Used)
			}
		}
	}
	usedrate := usedsum / disksum
	if !isProd {
		fmt.Printf("fetched %s , get value: %f\n", MetricDiskUsedRate, usedrate)
		fmt.Printf("fetched %s , get value: %f\n", MetricDiskFreeRate, 1-usedrate)
	}

	item1 = newReportReqItem(usedrate, MetricDiskUsedRate, err)
	item2 = newReportReqItem(1-usedrate, MetricDiskFreeRate, err)
	return
}

func fetchDiskIO(ctx context.Context, isProd bool) (item1, item2 *ReportReqItem) {
	readsum := 0.0
	writesum := 0.0
	ioInfo, err := disk.IOCountersWithContext(ctx)
	if err != nil {
		Logger().Error("failed to fetch disk io info", zap.Error(err))
	} else {
		for _, io := range ioInfo {
			readsum += float64(io.ReadBytes)
			writesum += float64(io.WriteBytes)
		}
	}
	if !isProd {
		fmt.Printf("fetched %s , get value: %f\n", MetricDiskWrite, writesum)
		fmt.Printf("fetched %s , get value: %f\n", MetricDiskRead, readsum)
	}
	item1 = newReportReqItem(readsum/DataUnit, MetricDiskRead, err)
	item2 = newReportReqItem(writesum/DataUnit, MetricDiskWrite, err)
	return
}
